{T}

如何调用 AI 接口生成组件

现在写组件,不再需要从头写,完全可以告诉 AI 你的需求,让 AI 给你生成。

AI 生成的代码基本就是可用的,然后再改一改就可以了。

会用 copilot、cursor 等工具来生成代码。

这样是可以,但是定制性不强。

如果你要做这种 bolt.new、v0 这种根据你的需求生成网站全部代码的工具呢?

是不是就没思路了?

这时候就要学会自己调用 AI 接口了。

来试一下:

首先需要一个 API KEY。

国内调用国外的 ai 接口有重重阻碍。

所以一般都是找个国内的代理商的 api 来用。

这种代理挺多的,我这里用的 302.ai,你也可以用别的

这种代理都是给你对接好了国内、国外各种模型,可以直接用。

我这里充了 5 美元,也就是 40 人民币,可以用支付宝支付,这点确实方便:

国外的那些模型,你支付都是个问题。

生成一个 API KEY:

接下来就可以调用 openai 的接口了。

这些代理都只是做了一下转发,所以接口和 openai 的一模一样。

创建个项目:

bash
mkdir openai-test
cd openai-test
npm init -y

安装下 openai 的包:

css
npm install --save openai

其实 AI 接口都是 rustful 的,但直接发请求比较麻烦,一般会基于 sdk 来调,也就是 npm 包。

创建 src/index.mjs

javascript
import OpenAI from 'openai';

const client = new OpenAI({
    apiKey: '你的 API KEY',
    baseURL: 'https://api.302.ai/v1'
});

async function main() {
  const stream = await client.chat.completions.create({
    model: "gpt-4",
    messages: [
      {role: 'user', content: '生成一个 Table 的 React 组件'}
    ],
    stream: true
  });

  for await (const chunk of stream) {
    process.stdout.write(chunk.choices[0]?.delta?.content || '');
  }
}

main();

.mjs 是告诉 node 这个文件是 es module 的。

这里要填入 baseURL 和 apiKey,这俩都用代理商的。

model 是指定用哪个模型。

messages 是上下文,也就是聊天记录。

stream 指定 true 就是流式返回内容。

跑一下:

生成的代码是用 table 写的:

如何再告诉它让它不要用 table,并且引入 sass 写下样式呢?

这样:

把回答放到一个 md 里。

然后在 messages 数组里加上这条聊天记录:

messages 数组就是上下文,如果基于之前聊的内容继续聊,那就把这些信息放到 message 数组里。

这里 role 为 asssitant 就是 AI 说的内容,role 为 user 就是用户说的内容。

javascript
const stream = await client.chat.completions.create({
    model: "gpt-4",
    messages: [
      {role: 'user', content: '生成一个 Table 的 React 组件'},
      {role: 'assistant', content: fs.readFileSync('./src/response1.md', 'utf-8')},
      {role: 'user', content: '在这个基础上加上 sass 写下样式,并且不要用 table'}
    ],
    stream: true
});

再跑下:

可以看到,它对之前的 Table 组件做了下修改,没用 table 标签了,并且加上了 sass 来写样式。

但现在生成的代码比较随意,实际上我们的项目都是有代码规范的。

如何指定生成代码的规范,回答的格式呢?

在 system 里指定:

创建 system.md

markdown
# Role: 前端工程师

## Profile

- author: 神光
- language: 中文
- description: 你非常擅长写 React 组件

## Goals

- 根据用户需求生成组件代码

## Skills

- 熟练掌握 typescript

- 会写高质量的 React 组件

## Constraints

- 用到的组件来源于 antd

- 样式用 scss 写

## Workflows

根据用户描述生成的组件,规范如下:

组件包含 4 类文件:

    1、index.ts
    这个文件中的内容如下:
    export { default as [组件名] } from './[组件名]';
    export type { [组件名]Props } from './interface';

    2、interface.ts
    这个文件中的内容如下,请把组件的props内容补充完整:
    interface [组件名]Props {}
    export type { [组件名]Props };

    4、[组件名].tsx
    这个文件中存放组件的真正业务逻辑,不能编写内联样式,如果需要样式必须在 5、styles.ts 中编写样式再导出给本文件用

    5、styles.scss
    这个文件中必须用 scss 给组件写样式,导出提供给 4、[组件名].tsx

## Initialization

作为前端工程师,你知道你的[Goals],掌握技能[Skills],记住[Constraints], 与用户对话,并按照[Workflows]进行回答,提供组件生成服务

就是通过 markdown 来指定这个机器人都会啥,也就是角色。

你用的一些 AI 聊天工具基本都内置了很多 system 设置:

比如 AI 菩萨、人生导师啥的。

在 messages 里指定这个 system 设置:

这样,AI 的回答方式就被限定了。

至此,system、user、assistant 这三个 role 就都知道是啥了。

跑一下:

现在的回答是不是顺眼多了?

要求的 index.ts、interface.ts、Table.tsx、styles.scss 就都给生成了。

那如何把这些代码提取出来跑呢?

手动复制当然是可以的,但是要做自动化工具,自然就要把这一步自动化了。

主流的方案其实很简单,就是正则匹配。

比如 chatgpt-artifacts

他就是可以提取回答中的代码并运行:

它的实现原理就是正则匹配代码段:

所以要做 bolt.new 这种工具也可以用正则匹配把代码摘出来:

当然,其实还有种更方便的方式,就是 tools,之前叫 function calls。

用了它之后就不返回消息了,而是返回一个函数调用,具体的参数格式可以指定。

这样用:

传入 tools 数组,创建一个 getCode 的函数,指定参数是一个对象,有 code1、code2、code3、code4 这四个属性,内容分别是 4 个文件的内容。

这次不用 stream 返回了:

javascript
import OpenAI from 'openai';
import fs from 'node:fs';

const client = new OpenAI({
    apiKey: '你的 API KEY',
    baseURL: 'https://api.302.ai/v1'
});

async function main() {
  const stream = await client.chat.completions.create({
    model: "gpt-4",
    messages: [
      {role: 'system', content: fs.readFileSync('./src/system.md', 'utf-8')},
      {role: 'user', content: '生成一个 Table 的 React 组件'},
      {role: 'assistant', content: fs.readFileSync('./src/response1.md', 'utf-8')},
      {role: 'user', content: '在这个基础上加上 sass 写下样式,并且不要用 table'}
    ],
    tools: [
      {
        type: "function",
        function: {
          name: "getCode",
          description: "生成的组件代码",
          parameters: {
            type: "object",
            properties: {
              code1: {
                type: "string",
                description: "生成的 index.ts 代码"
              },
              code2: {
                type: "string",
                description: "生成的 interface.ts 代码"
              },
              code3: {
                type: "string",
                description: "生成的 [组件名].tsx 代码"
              },
              code4: {
                type: "string",
                description: "生成的 styles.ts 代码"
              },
            },
            required: ["code1", 'code2', 'code3', 'code4']
          }
        }
      },
    ],
    // stream: true
  });

  console.log(stream.choices[0].message.tool_calls[0].function)

}

main();

跑一下:

可以看到,返回的不就是想要的格式么?

还用自己正则解析么?

直接 parse 下这个 JSON 就行。

生成的代码我发现用到了 UserList,这个是不需要的,改下 prompt:

javascript
{role: 'user', content: '在这个基础上加上 sass 写下样式,并且不要用 table,有 name、age、email 三列,数据是参数传入的'}

再跑下:

复制出来,parse 下,然后写入文件:

创建 test.mjs

javascript
import fs from 'node:fs';

const res = {
    name: 'getCode',
    arguments: '{
' +
      `"code1": "export { default as UserTable } from './UserTable';\
export type { UserTableProps } from './interface';\
",
` +
      '"code2": "interface UserTableProps {\
    users: {\
        name: string;\
        age: number;\
        email: string;\
    }[];\
}\
export type { UserTableProps };\
",
' +
      `"code3": "import React from 'react';\
import { UserTableProps } from './interface';\
import './styles.scss';\
\
const UserTable: React.FC<UserTableProps> = ({users}) => {\
    return (\
        <div className='user-table'>\
            <div className='user-table-row user-table-header'>\
                <div className='user-table-cell'>Name</div>\
                <div className='user-table-cell'>Age</div>\
                <div className='user-table-cell'>Email</div>\
            </div>\
            {users.map((user, index) => (\
                <div key={index} className='user-table-row'>\
                    <div className='user-table-cell'>{user.name}</div>\
                    <div className='user-table-cell'>{user.age}</div>\
                    <div className='user-table-cell'>{user.email}</div>\
                </div>\
            ))}\
        </div>\
    )\
};\
\
export default UserTable;\
",
` +
      '"code4": ".user-table {\
    display: flex;\
    flex-direction: column;\
    margin: 20px;\
}\
\
.user-table-row {\
    display: flex;\
}\
\
.user-table-row.user-table-header {\
    font-weight: bold;\
}\
\
.user-table-cell {\
    flex: 1;\
    padding: 10px;\
    border: 1px solid #ddd;\
}\
"
' +
      '}'
  }

const codes = JSON.parse(res.arguments);

fs.mkdirSync('./Table');
fs.writeFileSync('./Table/index.ts', codes.code1);
fs.writeFileSync('./Table/interface.ts', codes.code2);
fs.writeFileSync('./Table/UserTable.tsx', codes.code3);
fs.writeFileSync('./Table/styles.scss', codes.code4);

跑一下:

bash
node ./src/test.mjs

可以看到,写入的文件都是对的。

然后创建个 vite 项目,把它放进去试试:

lua
npx create-vite react-test-project

安装下依赖:

css
npm install
npm install --save-dev sass

把 Table 目录复制到 src,在 App.tsx 引入下:

javascript
import { UserTable } from './Table'

function App() {
  return <UserTable users={[
    {
      name: 'guang',
      age: 20,
      email: 'guang@guang.com'
    },
    {
      name: 'dong',
      age: 18,
      email: 'dong@dong.com'
    }
  ]}/>
}

export default App

还要去掉 main.tsx 里的 index.css

跑一下:

arduino
npm run dev

可以看到,Table 组件正常渲染出来了:

当然,这个组件比较简单,你可以让 AI 生成更复杂的组件,或者生成之后让它继续完善。

但流程是一样的。

这样,是不是可以写个 CLI 工具,输入组件名、组件描述,让 AI 生成符合规范的代码,然后写入文件呢?

或者像 bolt.new 这样,生成整个项目的代码,然后跑起来呢?

其实都是可以的。

代码上传了文档仓库

总结

本文学了下如何调用 openai 的接口。

通过 openai 的 npm 包来调,找一个国内代理商的 API_KEY 就可以了。

messages 就是上下文,可以指定三种 role:user 是用户,assistant 是 AI,system 是预先对 AI做一些设置,比如让它输出的代码都符合某种规范,用什么格式回答等。

这样回答至少是按照规定的格式来的。

不符合需求的话可以让 AI 再改,把聊天过程添加到 messages 数组(也就是上下文)就可以了。

提取回答中的代码,主流的方案是正则匹配,但今天用了 tools 的方式来提取的代码,它是让 AI 的回答是按照函数调用的方式,生成某种格式的参数。再解析参数 json 就可以了。

之后把生成的代码写入磁盘,跑了下试试,能够正常跑。

各种 AI 生成代码的工具其实都可以自己做,或者你可以做个 CLI 工具、或者做个 VSCode 插件、或者做个网站,都是可以的。

只会用 AI 工具是不够的,你还得会调 AI 接口,这样才能定制,做一些特定场景的提效工具。